/* 
 * Copyright 2010 Ziya Guven Koylu, Ozge Koylu
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package com.koylu.caffein.proc;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.koylu.caffein.exception.ClazzConfigException;
import com.koylu.caffein.javassist.BeanGenerator;
import com.koylu.caffein.model.caffein.CaffeinConfig;

public class Proc {
	private static Log log = LogFactory.getLog(Proc.class);
	private String name;
	private String type;
	private String schema;
	private boolean ready = Boolean.FALSE;
	private List<Param> params;
	private List<Param> paramsIn;
	private List<Param> paramsOut;
	private ReturnParam returnParam;
	
	public String getName() {
		return name;
	}
	
	public void setName(String name) {
		this.name = name;
	}
	
	private String procedureWithSchema;
	public String getProcedureWithSchema() {
		if(procedureWithSchema == null){
			if( schema == null || "".equals(schema) ){
				procedureWithSchema = name;
			}else{
				procedureWithSchema = schema+"."+name;
			}
		}
		return procedureWithSchema;
	}
	
	public String getType() {
		return type;
	}
	
	public void setType(String type) {
		this.type = type;
	}
	
	public String getSchema() {
		return schema;
	}
	
	public void setSchema(String schema) {
		this.schema = schema;
	}
	
	public List<Param> getParams() {
		return params;
	}
	
	public Object toEmptyObject(CaffeinConfig caffeinConfig) throws Exception {
		return getRealClass(caffeinConfig).newInstance();
	}
	
	private Class<?> realClass;
	public Class<?> getRealClass(CaffeinConfig caffeinConfig) throws Exception{
		if(realClass == null){
			realClass = toVirtualClass(caffeinConfig);
		}
		return realClass;
	}
	
	private Class<?> virtualClass;
	public Class<?> toVirtualClass(CaffeinConfig caffeinConfig) throws Exception {
		if(virtualClass == null){
			if( type == null || "".equals(type)  ){
				BeanGenerator beanGenerator = new BeanGenerator(name);
				if(params != null){
					for(Param param : params){
						beanGenerator.addProperty(param.getName(), param.toVirtualClass(caffeinConfig));
					}	
				}
				if(returnParam != null){
					beanGenerator.addProperty(returnParam.getName(), returnParam.toVirtualClass(caffeinConfig));
				}
				virtualClass = beanGenerator.createClass();
			}else{
				if(BeanGenerator.isClassExist(type)){
					log.info("class found in the classpath:"+type);
					virtualClass = Class.forName(type);
				}else{
					throw new ClazzConfigException("class not found:"+type);
				}				
			}
		}
		return virtualClass;
	}
	
	public void addParam(Param param){
		if(params == null){
			params = new ArrayList<Param>();
		}
		for (Iterator<Param> iterator = params.iterator(); iterator.hasNext();) {
			if(param.getColumn().equals(iterator.next().getColumn())){
				throw new RuntimeException("param already exist-->"+param.getColumn());
			}
		}
		param.setProc(this);
		if(param instanceof ReturnParam){
			if(this.returnParam == null){
				this.returnParam = (ReturnParam) param;	
			}else{
				throw new RuntimeException("more than one return-param");
			}
		}else{
			params.add(param);
			param.setIndex(params.size());			
		}
	}
	
	public void prepare(CaffeinConfig caffeinConfig) throws Exception{
		if(params != null){
			for(Param param:params){
				if(param.getColumnType(caffeinConfig) == 1){//IN
					if(paramsIn == null){
						paramsIn = new ArrayList<Param>();
					}
					paramsIn.add(param);
				}else if(param.getColumnType(caffeinConfig) == 4){//OUT
					if(paramsOut == null){
						paramsOut = new ArrayList<Param>();
					}
					paramsOut.add(param);
				}else if(param.getColumnType(caffeinConfig) == 2){//INOUT
					if(paramsIn == null){
						paramsIn = new ArrayList<Param>();
					}
					paramsIn.add(param);
					if(paramsOut == null){
						paramsOut = new ArrayList<Param>();
					}
					paramsOut.add(param);
				}
				if(returnParam != null){
					param.setIndex(param.getIndex()+1);
				}
			}
			if(returnParam != null){
				if(paramsOut == null){
					paramsOut = new ArrayList<Param>();
				}
				paramsOut.add(returnParam);
			}
		}
		ready = Boolean.TRUE;
	}
	
	private StringBuffer query = null;
	public void call(CaffeinConfig caffeinConfig, Object object) throws Exception{
		if(!ready){
			prepare(caffeinConfig);
		}
		if(query == null){
			query = new StringBuffer("{");
			if(returnParam != null){
				query.append("?").append(" = ");
			}
			query.append("call ").append(getProcedureWithSchema()).append("(");
			for (Iterator<Param> iterator = params.iterator(); iterator.hasNext();) {
				iterator.next();
				query.append("?");
				if(iterator.hasNext()){
					query.append(", ");
				}
			}
			query.append(")}");	
		}
		if(caffeinConfig.isEnableQueryLogging()){
			log.info(query.toString());
		}
		Connection connection = null;
		CallableStatement callableStatement = null;
		try {
			connection = caffeinConfig.getConnection();
			callableStatement = connection.prepareCall(query.toString());
			if(paramsIn != null){
				for(Param param:paramsIn){
					param.getConverter(caffeinConfig).fieldToColumn(callableStatement, PropertyUtils.getProperty(object, param.getName()), param.isColumnNullable(caffeinConfig), param.getIndex());
				}				
			}
			if(paramsOut != null){
				for(Param param:paramsOut){
					callableStatement.registerOutParameter(param.getIndex(), param.getColumnDataType(caffeinConfig));
				}				
			}
			callableStatement.execute();
			if(paramsOut != null){
				for(Param param:paramsOut){
					PropertyUtils.setProperty(object, param.getName(), param.getConverter(caffeinConfig).columnToField(callableStatement, param.getIndex()));
				}
			}
		} catch (Exception e) {
			throw e;
		}finally{
			try {
				if(callableStatement != null){
					callableStatement.close();
				}
			} catch (Exception e) {}
			try {
				if(connection != null){
					connection.close();
				}
			} catch (Exception e) {}
		}
	}
	
	public void call(CaffeinConfig caffeinConfig, Map<String, Object> params) throws Exception{
		call(caffeinConfig, (Object)params);
	}
	
	public String toString() {
		StringBuffer buffer = new StringBuffer("Name:").append(getName()).append(" Schema:").append(getSchema()).append(" Type:").append(getType()).append("\n");
		if(params != null){
			for(Param param:params){
				buffer.append(param.toString()).append("\n");
			}	
		}
		if(returnParam != null){
			buffer.append(returnParam.toString());
		}
		return buffer.toString();
	}
}